跳到主要内容

Redis 持久化数据 ~

持久化数据

Redis 是一个把数据保存到内存的数据库,当 Redis 服务器重启时,数据可能就会丢失,所以需要持久化到硬盘

目前有两种持久化方式

  1. RDB(默认):就是一定时间间隔检测 key 的变化情况,然后持久化数据
  2. AOF:日志记录的方式,可以记录每一条命令的操作,每次一命令操作后来持久化数据(和 mysql 差不多了..)

RDB 方式

RDB 文件的创建可以手动触发,也可以自动触发。

注意:使用 RDB 的方式就是用的快照,所谓内存快照,顾名思义就是给内存拍个照,在某个时刻把内存中的数据记录下来,以文件的形式保存到硬盘上,这样即使宕机,数据依然存在。在服务器重启后只需要把 “照片” 中的数据恢复即可。RDB 持久化就是把当前进程的数据在某个时刻生成快照(一个压缩的二进制文件)保存到硬盘的过程,触发 RDB 持久化过程分为手动触发和自动触发。

确定当前是否启用了

要确定当前 Redis 是否启用了持久化,可以通过以下几种方式来检查:

  1. 查看 Redis 配置文件:Redis 的配置文件通常是 redis.conf。你可以打开该文件,并搜索名为 save 的配置项。如果配置项中有内容,则表示已启用持久化。例如,以下配置表示每发生一次修改就将数据异步保存到磁盘上:

    save 1 1

    如果配置项中没有内容,则表示未启用持久化。

  2. 使用 Redis 命令行界面:可以通过连接 Redis 服务器并执行 CONFIG GET save 命令来获取持久化配置。如果返回结果中包含非空值,则表示已启用持久化。例如,返回结果为:

    1) "save"
    2) "900 1 300 10 60 10000"

    其中的第二个值表示已启用持久化。

  3. 使用 Redis INFO 命令:连接 Redis 服务器,并执行 INFO 命令。该命令将返回 Redis 服务器的信息。在返回的信息中,可以搜索 rdb_savesaof_enabled 两个字段。如果它们的值为 1,则表示启用了 RDB 持久化和 AOF 持久化。

    # Server
    redis_version:5.0.5
    redis_git_sha1:00000000
    ...
    rdb_saves:1
    ...
    aof_enabled:1

以上方法可以帮助你确定当前 Redis 是否开启了持久化。持久化功能可以确保在 Redis 重启或崩溃后数据的持久性,以防止数据丢失。

手动触发

手动触发分别对应 save 和 bgsave 命令:

1、save 命令

save 命令会阻塞当前 Redis 服务器,直到 RDB 过程完成为止。在服务器进程阻塞期间,服务器不能处理任何命令请求。因此,当 save 命令正在执行时,客户端发送的所有命令都会被拒绝,直到 save 命令执行完毕。

redis> save #等待,直到RDB文件创建完毕
ok

注意:Redis 的单线程模型就决定了,我们要尽量避免所有会阻塞主线程的操作,由于 Save 命令执行期间阻塞服务器进程,对于内存比较大的实例会造成长时间阻塞,因此线上环境不建议使用。

2、bgsave 命令

bgsave 命令会派生出一个子进程(而不是线程),由子进程进行 RDB 文件创建,而父进程继续处理命令。

redis>bgsave
Background saving started # 直接返回,由子进程进行RDB文件创建
redis> # 继续处理其它命令

注意:

  1. bgsave 命令执行的时候,为了避免父进程与子进程同时执行两个 rdbSave 的调用而产生竞争条件,客户端发送的 save 命令会被服务器拒绝。
  2. 如果 bgsave 命令正在执行,bgrewriteaof(aof重写)命令会被延迟到 bgsave 命令之后执行,如果 bgrewriteaof 命令正在执行,那么客户端发送的 bgsave 命令会被服务器拒绝。
  3. 虽然 bgsave 命令是由子进程进行RDB文件的生成,但是 fork() 创建子进程的时候会阻塞父进程(详情请往下看)。

自动触发

因为 bgsave 命令可以在不阻塞服务器进程的情况下保存,所以 redis 可以通过设置服务器配置的 save 选项,让服务器每隔一段时间自动执行一次 bgsave命令。

如:我们向服务器设置如下配置(这也是 redis 默认的配置):

#   配置保存时间的设置项
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed

save 900 1
save 300 10
save 60 10000

那么只要满足如下条件中的一个 bgsave 命令就会被执行:

  • 服务器在 900秒内对数据库进行了至少 1次修改
  • 服务器在 300秒内对数据库进行了至少 10次修改
  • 服务器在 60秒内对数据库进行了至少 10000次修改

然后启动就不是直接使用 redis-server 启动了(如下所示),需要在后面跟一个配置文件(注意,要给权限先)

redis-server  /etc/redis/redis.conf

注意:默认开启一个 redis-server,它是一个守护进程,所以关闭了当前的 shell 也会给挂到后台去

RDB文件的载入

在 Redis 启动的时候,只要检测到 RDB 文件的存在,就会自动加载 RDB 文件。需要注意的是

因为 AOF 文件的更新频率通常比 RDB 文件的更新频率高,所以口如果服务器开启了 AOF 持久化功能,那么服务器会优先使用 AOF 文件来还原数据库状态。

只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态。

注意:服务器在载入 RDB 文件期间,会一直处于阻塞状态,直到载入工作完成为止

内存快照的问题

了解了什么是 Redis 的 RDB 持久化,我们来思考两个问题

快照的时候数据可以修改吗?

Redis RDB 持久化是对某一时刻的内存中的全量数据进行拍照。这让我们不得不思考,快照的时候数据可以修改吗?

首先,如果我们使用 save 命令做持久化,那么由于 Redis 单线程模型的原因,在持久化的过程中会阻塞,是不能执行其它命令的。也许有人会说可以使用 bgave 命令,但使用 bgsave 就没有问题了吗?

我们在拍照的时候,通常摄影师是不让我们动的,因为一动可能照片就模糊了。在 Redis 进行内存快照的时候也会如此。如果我们持久化的过程中,有些数据被修改了。那么就会破坏快照的正确性与完整性。

比如在 t 时刻,我们对内存进行快照,此时我们希望的是记录下来 t 时刻内存中所有的数据,假设我们的 RDB 操作需要 10s的时间,而 t+2s 我们执行了一个修改操作把 Key1 的值由 A 修改成了 B,而此时 RDB 操作却还没有把 Key1 的值写入磁盘。在 t+5s 的时候读取到 key1 的值写入磁盘。

那么此次快照记录的 Key1 的值就是 B,而不是 t 时刻的 A。这样就破坏了 RDB 文件的正确性。

RDB 文件的生成是需要时间的,如果快照执行期间数据不能被修改,对于业务系统来说不能接受的。那么 Redis 是如何解决这个问题的呢?

Redis 借助了操作系统提供的写时复制技术(Copy-On-Write, COW),可以让在执行快照的同时,正常处理写操作。

简单来说,bgsave fork 子进程的时候,并不会完全复制主进程的内存数据,而是只复制必要的虚拟数据结构,并不为其分配真实的物理空间,它与父进程共享同一个物理内存空间。

bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,此时会给子进程分配一块物理内存空间,把要修改的数据复制一份,生成该数据的副本到子进程的物理内存空间。

然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

可以频繁进行快照操作吗?

假设我们在 t 时刻做了一次快照,然后又在 t+n 时刻做了一次快照,而在这期间,发生了数据修改。而此时宕机了,那么,只能按照 t 时刻的快照进行恢复。那么这 n秒的数据就彻底丢失无法恢复了。

所以,要想尽可能恢复数据,就只能缩短快照执行的时间间隔,间隔的时间越小,丢失数据也就越少。那么可以频繁的执行快照操作吗?

我们知道 bgsave 执行时并不阻塞主线程,但是这不代表可以频繁执行快照操作。

一方面,持久化是一个写入磁盘的过程,频繁将全量数据写入磁盘,会给磁盘带来很大压力,频繁执行快照也容易导致前一个快照还没有执行完,后一个又开始了,这样多个快照竞争有限的磁盘带宽,容易造成恶性循环。

再者,bgsave 所 fork 出来的子进程执行操作虽然并不会阻塞父进程的操作,但是 fork 出子进程的操作却是由主进程完成的,会阻塞主进程,fork 子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量 CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork阻塞时间也就越久。

也许有人会想到是否可以做增量快照呢?也就是只对上一次快照之后的数据做快照。

首先思路肯定是可以,但是增量快照要求记住哪些数据上一次快照之后产生的。这就需要额外的元数据来记录这些信息,会引入额外的空间消耗。这对于内存资源宝贵的 Redis 来说,并不是一个很好的方案。

如果不能频繁执行快照操作,那么该如何解决两次快照之间的数据丢失的问题呢?Redis 还提供了另外一种持久化方式——AOF(append to file)日志。

AOF 方式

AOF 日志持久化(Append Only File)

与内存快照保存当前内存中的数据所不同,AOF 持久化是通过保存 Redis 服务器所执行的写命令来记录数据库状态的。即每执行一个命令,就会把该命令写到日志文件里。

需要注意的是写日志的操作在 Redis 执行命令将数据写入内存之后,如下图所示:

这样做的好处就是不会阻塞当前操作,也可以避免额外的检查开销,如果是在命令执行前进行写日志的操作,一旦命令语法是错误的,不进行检查的话就会导致写入到日志文件中的命令是错误的,在使用日志文件恢复数据的时候就会出错。而在命令执行后在进行日志的写入则不会有这个问题。

但是也存在两个问题

  1. AOF 虽然避免了对当前命令的阻塞,但却可能会给下一个操作带来阻塞风险。因为,AOF 日志是在主进程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了
  2. 如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。

AOF 缓冲区

针对上面两个问题,Redis 提供了缓冲区的方式进行 AOF 日志的记录,以达到尽可能的避免阻塞和数据丢失的问题。即 Redis 在执行完命令进行持久化的时候,并非直接写入磁盘日志文件,而是先写入 AOF 缓冲区内,之后再通过某种策略写到磁盘。

使用缓存区的方式进行 AOF 日志的记录,上面提到的两个问题其实就和日志从缓冲区写入磁盘的时机有关系。

三种回写策略

Redis AOF 机制提供了三种回写磁盘的策略。

  • Always(同步写回):命令写入 AOF缓冲区后调用系统 fsync 操作同步到 AOF 文件,fsync完成后线程返回
  • Everysec(每秒写回):命令写入 AOF缓冲区后调用系统 write 操作,write完成后线程返回。fsync同步文件操作由专门线程每秒调用一次
  • No(操作系统自动写回):命令写入 AOF缓冲区后调用系统 write 操作,不对 AOF文件做 fsync同步,同步硬盘操作由操作系统负责,通常同步周期最长30秒

但其实可以看出这三种回写策略都并不能完美的解决问题

配置为 always 时,每次写入都要同步 AOF 文件,硬盘的写入速度无法与内存相提并论,显然与 Redis 高性能特性背道而驰

配置为 no 时,由于操作系统每次同步 AOF 文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但数据安全性无法保证。

配置为 everysec,是建议的同步策略,也是默认配置,虽然能做到兼顾性能和数据安全性。但极端情况下一会造成 1秒内的数据丢失。

在真正使用中,我们可以根据具体对性能和数据完整性的要求,分析这三种回写策略,选择适合的策略来进行持久化。

回写策略优点缺点
Always(同步写回)可靠性高、数据基本不丢失性能较差
Everysec(每秒写回)性能适中宕机时丢失1秒内的数据
No(操作系统自动写回)性能好宕机时丢失数据较多

AOF 的使用

AOF 默认是关闭的

修改配置文件的 appendonly 为 yes 表示开启 AOF

AOF 也有两种方式

# 每次操作都持久化一次
appendfsync always

# 每一秒操作一次
appendfsync everysec

# 不进行持久化
appendfsync no

在 Redis 服务器重启后,会优先去载入 AOF 日志文件。因为 AOF 文件里面包含了重建数据库状态所需的所有写命令,所以服务器重新执行一遍 AOF 文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。

而由于 Redis 命令只能在客户端上下文中执行,Redis 会创建一个没有网络连接的伪客户端来执行 AOF 文件中的内容。

AOF 重写

日志文件越来越大怎么办?

选择了合适的回写策略,AOF 这种持久化的方式还有其它问题吗?

因为 AOF 持久化是通过保存被执行的写命令来记录数据库状态的,所以随着时间的流逝,AOF 文件中的内容会越来越多,文件的体积也会越来越大,过大的 AOF 文件不仅追加命令会变慢,而且可能对 Redis 服务器、甚至整个宿主计算机造成影响,并且 AOF 文件的体积越大,使用 AOF 文件来进行数据还原所需的时间就越多。

这个时候就要用到 AOF 重写机制了

如下例子:

redis> set testKey testValue
OK
redis> set testKey testValue1
OK
redis> del testKey
OK
redis> set testKey hello
OK
redis> set testKey world
OK

AOF 文件是以追加的方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令。如上示例,我们执行完命令后,Redis 会在 AOF 里面追加 5条命令。但实际上只需要 set testKey world 一条命令就够了。

而 AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件,也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。

比如说,当读取了键值对 "testkey": "world" 之后,重写机制会记录 set testkey world 这条命令。这样,当需要恢复时,可以重新执行该命令,实现 "testkey": "world" 的写入。

这样,重写后的日志,从5条变成了1条,而对于可能被修改过成百上千次的键值对来说,重写能节省的空间就更大了。

虽然 AOF 重写后,日志文件会缩小,但是,要把整个数据库的最新数据的操作日志都写回磁盘,仍然是一个非常耗时的过程。这时,我们不得不关注:重写会不会导致阻塞?这就要看看 AOF 重写的过程是怎么样的

AOF 重写过程

因为 AOF 重写也是一个非常耗时的过程,又因为 Redis 单线程的特性,同内存快照一样,AOF 重写的过程也是由父进程 fork 出 bgrewriteaof 子进程来完成的。

使用子进程(而不是开启一个线程)进行 AOF 重写虽然可以避免使用锁的情况下,保证数据安全性,但是会带来子进程和父进程一致性问题。

例如在开始重写之后父进程又接收了新的键值对此时子进程是无法知晓的,当子进程重写完成后的数据库和父进程的数据库状态是不一致的。

如下表:

时间服务器进程(父进程)子进程
T1执行命令 SET K1 V1
T2执行命令 SET K1 V1
T3创建子进程,执行AOF文件重写开始AOF重写
T4执行命令 SET K2 V2执行重写
T5执行命令 SET K3 V3执行重写
T6执行命令 SET K4 V4完成AOF重写

在 T6 时刻服务器进程有了 4个键,而子进程却只有 1个键

为了解决这种不一致性,Redis 设置了一个 AOF 重写缓冲区。

在子进程执行 AOF 重写期间。服务器进程需要执行以下 3个动作:

  1. 执行客户端命令
  2. 执行后追加到 AOF 缓冲区
  3. 执行后追加到 AOF 重写缓冲区

子进程完成 AOF 重写后,它向父进程发送一个信号,父进程收到信号后会调用一个信号处理函数,该函数把 AOF 重写缓冲区的命令追加到新 AOF 文件中然后替换掉现有 AOF 文件。

父进程处理完毕后可以继续接受客户端命令调用,可以看出在 AOF 后台重写过程中只有这个信号处理函数会阻塞服务器进程。

下表是完整的 AOF 后台重写过程:

时间服务器进程(父进程)子进程
T1执行命令 SET K1 V1
T2执行命令 SET K1 V1
T3创建子进程,执行AOF文件重写开始AOF重写
T4执行命令 SET K2 V2执行重写
T5执行命令 SET K3 V3执行重写
T6执行命令 SET K4 V4完成AOF重写,向父进程发送信号
T7接收到信号,将T4 T5 T6 服务器的写命令追加到新的AOF文件末尾
T8用新的 AOF 替换旧的 AOF

这样就可以保证重写日志期间的所有操作也都会写入新的 AOF 文件。

需要注意的是, T7 T8 执行的任务会阻塞服务器处理命令。

总的来说,就是每次 AOF 重写时,Redis 会先 fork 出一个子进程用于重写;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。

Reference